Un guide complet pour optimiser les DataFrames Pandas en termes d'utilisation mémoire et de performances, couvrant les types de données, l'indexation et les techniques avancées.
Optimisation des DataFrames Pandas : Utilisation de la mémoire et réglage des performances
Pandas est une puissante bibliothèque Python pour la manipulation et l'analyse de données. Cependant, lorsque l'on travaille avec de grands ensembles de données, les DataFrames Pandas peuvent consommer une quantité importante de mémoire et présenter des performances lentes. Cet article fournit un guide complet pour optimiser les DataFrames Pandas à la fois pour l'utilisation de la mémoire et pour les performances, vous permettant de traiter des ensembles de données plus volumineux plus efficacement.
Comprendre l'utilisation de la mémoire dans les DataFrames Pandas
Avant de plonger dans les techniques d'optimisation, il est crucial de comprendre comment les DataFrames Pandas stockent les données en mémoire. Chaque colonne d'un DataFrame a un type de données spécifique, qui détermine la quantité de mémoire requise pour stocker ses valeurs. Les types de données courants incluent :
- int64 : entiers 64 bits (par défaut pour les entiers)
- float64 : nombres à virgule flottante 64 bits (par défaut pour les nombres à virgule flottante)
- object : objets Python (utilisé pour les chaînes de caractères et les types de données mixtes)
- category : données catégorielles (efficace pour les valeurs répétitives)
- bool : valeurs booléennes (True/False)
- datetime64 : valeurs de date et heure
Le type de données object est souvent le plus gourmand en mémoire car il stocke des pointeurs vers des objets Python, qui peuvent être nettement plus volumineux que les types de données primitifs comme les entiers ou les flottants. Les chaînes de caractères, même courtes, lorsqu'elles sont stockées en tant que object, consomment beaucoup plus de mémoire que nécessaire. De même, utiliser int64 alors que int32 suffirait gaspille de la mémoire.
Exemple : Inspection de l'utilisation mémoire d'un DataFrame
Vous pouvez utiliser la méthode memory_usage() pour inspecter l'utilisation mémoire d'un DataFrame :
import pandas as pd
import numpy as np
data = {
'col1': np.random.randint(0, 1000, 100000),
'col2': np.random.rand(100000),
'col3': ['A', 'B', 'C'] * (100000 // 3 + 1)[:100000],
'col4': ['This is a long string'] * 100000
}
df = pd.DataFrame(data)
memory_usage = df.memory_usage(deep=True)
print(memory_usage)
print(df.dtypes)
L'argument deep=True garantit que l'utilisation de la mémoire des objets (comme les chaînes de caractères) est calculée avec précision. Sans deep=True, seule la mémoire des pointeurs sera calculée, et non celle des données sous-jacentes.
Optimisation des types de données
L'un des moyens les plus efficaces de réduire l'utilisation de la mémoire est de choisir les types de données les plus appropriés pour les colonnes de votre DataFrame. Voici quelques techniques courantes :
1. Réduction des types de données numériques (Downcasting)
Si vos colonnes d'entiers ou de nombres à virgule flottante ne nécessitent pas toute la plage de précision des 64 bits, vous pouvez les convertir en types de données plus petits comme int32, int16, float32, ou float16. Cela peut réduire considérablement l'utilisation de la mémoire, en particulier pour les grands ensembles de données.
Exemple : Considérez une colonne représentant l'âge, qui a peu de chances de dépasser 120. La stocker en tant que int64 est un gaspillage ; int8 (plage de -128 à 127) serait plus approprié.
def downcast_numeric(df):
"""Réduit les colonnes numériques au plus petit type de données possible."""
for col in df.columns:
if pd.api.types.is_integer_dtype(df[col]):
df[col] = pd.to_numeric(df[col], downcast='integer')
elif pd.api.types.is_float_dtype(df[col]):
df[col] = pd.to_numeric(df[col], downcast='float')
return df
df = downcast_numeric(df.copy())
print(df.memory_usage(deep=True))
print(df.dtypes)
La fonction pd.to_numeric() avec l'argument downcast est utilisée pour sélectionner automatiquement le plus petit type de données possible pouvant représenter les valeurs de la colonne. Le copy() évite de modifier le DataFrame original. Vérifiez toujours la plage de valeurs de vos données avant de faire un downcasting pour vous assurer de ne pas perdre d'informations.
2. Utilisation des types de données catégoriels
Si une colonne contient un nombre limité de valeurs uniques, vous pouvez la convertir en un type de données category. Les types de données catégoriels stockent chaque valeur unique une seule fois, puis utilisent des codes entiers pour représenter les valeurs dans la colonne. Cela peut réduire considérablement l'utilisation de la mémoire, en particulier pour les colonnes avec une forte proportion de valeurs répétées.
Exemple : Considérez une colonne représentant des codes de pays. Si vous traitez un ensemble limité de pays (par exemple, uniquement les pays de l'Union européenne), la stocker en tant que catégorie sera beaucoup plus efficace que de la stocker sous forme de chaînes de caractères.
def optimize_categories(df):
"""Convertit les colonnes de type 'object' à faible cardinalité en type catégoriel."""
for col in df.columns:
if df[col].dtype == 'object':
num_unique_values = len(df[col].unique())
num_total_values = len(df[col])
if num_unique_values / num_total_values < 0.5:
df[col] = df[col].astype('category')
return df
df = optimize_categories(df.copy())
print(df.memory_usage(deep=True))
print(df.dtypes)
Ce code vérifie si le nombre de valeurs uniques dans une colonne de type object est inférieur à 50 % du nombre total de valeurs. Si c'est le cas, il convertit la colonne en un type de données catégoriel. Le seuil de 50 % est arbitraire et peut être ajusté en fonction des caractéristiques spécifiques de vos données. Cette approche est plus bénéfique lorsque la colonne contient de nombreuses valeurs répétées.
3. Éviter les types de données 'object' pour les chaînes de caractères
Comme mentionné précédemment, le type de données object est souvent le plus gourmand en mémoire, surtout lorsqu'il est utilisé pour stocker des chaînes de caractères. Si possible, essayez d'éviter d'utiliser les types de données object pour les colonnes de chaînes. Les types catégoriels sont préférables pour les chaînes à faible cardinalité. Si la cardinalité est élevée, demandez-vous si les chaînes peuvent être représentées par des codes numériques ou si les données de chaîne peuvent être évitées complètement.
Si vous devez effectuer des opérations sur les chaînes de la colonne, vous devrez peut-être la conserver en tant que type 'object', mais examinez si ces opérations peuvent être effectuées en amont, puis converties en un type plus efficace.
4. Données de date et d'heure
Utilisez le type de données datetime64 pour les informations de date et d'heure. Assurez-vous que la résolution est appropriée (une résolution à la nanoseconde peut être inutile). Pandas gère les données de séries temporelles de manière très efficace.
Optimisation des opérations sur les DataFrames
En plus d'optimiser les types de données, vous pouvez également améliorer les performances des DataFrames Pandas en optimisant les opérations que vous effectuez dessus. Voici quelques techniques courantes :
1. Vectorisation
La vectorisation est le processus consistant à effectuer des opérations sur des tableaux ou des colonnes entières à la fois, plutôt que d'itérer sur des éléments individuels. Pandas est hautement optimisé pour les opérations vectorisées, donc leur utilisation peut améliorer considérablement les performances. Évitez les boucles explicites chaque fois que possible. Les fonctions intégrées de Pandas sont généralement beaucoup plus rapides que les boucles Python équivalentes.
Exemple : Au lieu d'itérer à travers une colonne pour calculer le carré de chaque valeur, utilisez la fonction pow() :
# Inefficace (utilisation d'une boucle)
import time
start_time = time.time()
results = []
for value in df['col2']:
results.append(value ** 2)
df['col2_squared_loop'] = results
end_time = time.time()
print(f"Temps de la boucle : {end_time - start_time:.4f} secondes")
# Efficace (utilisation de la vectorisation)
start_time = time.time()
df['col2_squared_vectorized'] = df['col2'] ** 2
end_time = time.time()
print(f"Temps vectorisé : {end_time - start_time:.4f} secondes")
L'approche vectorisée est généralement des ordres de grandeur plus rapide que l'approche basée sur une boucle.
2. Utiliser `apply()` avec prudence
La méthode apply() vous permet d'appliquer une fonction à chaque ligne ou colonne d'un DataFrame. Cependant, elle est généralement plus lente que les opérations vectorisées car elle implique d'appeler une fonction Python pour chaque élément. N'utilisez apply() que lorsque les opérations vectorisées ne sont pas possibles.
Si vous devez utiliser `apply()`, essayez de vectoriser autant que possible la fonction que vous appliquez. Envisagez d'utiliser le décorateur `jit` de Numba pour compiler la fonction en code machine afin d'obtenir des améliorations de performance significatives.
from numba import jit
@jit(nopython=True)
def my_function(x):
return x * 2 # Fonction d'exemple
df['col2_applied'] = df['col2'].apply(my_function)
3. Sélectionner des colonnes efficacement
Lors de la sélection d'un sous-ensemble de colonnes d'un DataFrame, utilisez les méthodes suivantes pour des performances optimales :
- Sélection directe de colonnes :
df[['col1', 'col2']](le plus rapide pour sélectionner quelques colonnes) - Indexation booléenne :
df.loc[:, [True if col.startswith('col') else False for col in df.columns]](utile pour sélectionner des colonnes basées sur une condition)
Évitez d'utiliser df.filter() avec des expressions régulières pour sélectionner des colonnes, car cela peut être plus lent que d'autres méthodes.
4. Optimisation des jointures et des fusions
La jointure et la fusion de DataFrames peuvent être coûteuses en termes de calcul, en particulier pour les grands ensembles de données. Voici quelques conseils pour optimiser les jointures et les fusions :
- Utilisez des clés de jointure appropriées : Assurez-vous que les clés de jointure ont le même type de données et sont indexées.
- Spécifiez le type de jointure : Utilisez le type de jointure approprié (par ex.,
inner,left,right,outer) en fonction de vos besoins. Une jointure interne est généralement plus rapide qu'une jointure externe. - Utilisez `merge()` plutôt que `join()` : La fonction
merge()est plus polyvalente et souvent plus rapide que la méthodejoin().
Exemple :
df1 = pd.DataFrame({'key': ['A', 'B', 'C', 'D'], 'value1': [1, 2, 3, 4]})
df2 = pd.DataFrame({'key': ['B', 'D', 'E', 'F'], 'value2': [5, 6, 7, 8]})
# Jointure interne efficace
df_merged = pd.merge(df1, df2, on='key', how='inner')
print(df_merged)
5. Éviter les copies inutiles de DataFrames
De nombreuses opérations Pandas créent des copies de DataFrames, ce qui peut être gourmand en mémoire et prendre du temps. Pour éviter les copies inutiles, utilisez l'argument inplace=True lorsqu'il est disponible, ou réaffectez le résultat d'une opération au DataFrame d'origine. Soyez très prudent avec inplace=True car cela peut masquer des erreurs et rendre le débogage plus difficile. Il est souvent plus sûr de réaffecter, même si c'est légèrement moins performant.
Exemple :
# Inefficace (crée une copie)
df_filtered = df[df['col1'] > 500]
# Efficace (modifie le DataFrame original sur place - ATTENTION)
df.drop(df[df['col1'] <= 500].index, inplace=True)
#PLUS SÛR - réaffecte, évite inplace
df = df[df['col1'] > 500]
6. Traitement par lots (Chunking) et itération
Pour les ensembles de données extrêmement volumineux qui ne peuvent pas tenir en mémoire, envisagez de traiter les données par lots (chunks). Utilisez le paramètre chunksize lors de la lecture de données à partir de fichiers. Itérez sur les lots et effectuez votre analyse sur chaque lot séparément. Cela nécessite une planification minutieuse pour s'assurer que l'analyse reste correcte, car certaines opérations nécessitent de traiter l'ensemble du jeu de données en une seule fois.
# Lire le CSV par lots
for chunk in pd.read_csv('large_data.csv', chunksize=100000):
# Traiter chaque lot
print(chunk.shape)
7. Utiliser Dask pour le traitement parallèle
Dask est une bibliothèque de calcul parallèle qui s'intègre de manière transparente avec Pandas. Elle vous permet de traiter de grands DataFrames en parallèle, ce qui peut considérablement améliorer les performances. Dask divise le DataFrame en partitions plus petites et les distribue sur plusieurs cœurs ou machines.
import dask.dataframe as dd
# Créer un DataFrame Dask
ddf = dd.read_csv('large_data.csv')
# Effectuer des opérations sur le DataFrame Dask
ddf_filtered = ddf[ddf['col1'] > 500]
# Calculer le résultat (ceci déclenche le calcul parallèle)
result = ddf_filtered.compute()
print(result.head())
Indexation pour des recherches plus rapides
La création d'un index sur une colonne peut accélérer considérablement les opérations de recherche et de filtrage. Pandas utilise les index pour localiser rapidement les lignes qui correspondent à une valeur spécifique.
Exemple :
# Définir 'col3' comme index
df = df.set_index('col3')
# Recherche plus rapide
value = df.loc['A']
print(value)
# Réinitialiser l'index
df = df.reset_index()
Cependant, la création de trop nombreux index peut augmenter l'utilisation de la mémoire et ralentir les opérations d'écriture. Ne créez des index que sur les colonnes qui sont fréquemment utilisées pour les recherches ou le filtrage.
Autres considérations
- Matériel : Envisagez de mettre à niveau votre matériel (CPU, RAM, SSD) si vous travaillez constamment avec de grands ensembles de données.
- Logiciel : Assurez-vous d'utiliser la dernière version de Pandas, car les nouvelles versions incluent souvent des améliorations de performance.
- Profilage : Utilisez des outils de profilage (par ex.,
cProfile,line_profiler) pour identifier les goulots d'étranglement de performance dans votre code. - Format de stockage des données : Envisagez d'utiliser des formats de stockage de données plus efficaces comme Parquet ou Feather au lieu de CSV. Ces formats sont colonnaires et souvent compressés, ce qui se traduit par des fichiers de plus petite taille et des temps de lecture/écriture plus rapides.
Conclusion
L'optimisation des DataFrames Pandas pour l'utilisation de la mémoire et les performances est cruciale pour travailler efficacement avec de grands ensembles de données. En choisissant les types de données appropriés, en utilisant des opérations vectorisées et en indexant vos données efficacement, vous pouvez réduire considérablement la consommation de mémoire et améliorer les performances. N'oubliez pas de profiler votre code pour identifier les goulots d'étranglement de performance et envisagez d'utiliser le traitement par lots ou Dask pour les ensembles de données extrêmement volumineux. En mettant en œuvre ces techniques, vous pouvez libérer tout le potentiel de Pandas pour l'analyse et la manipulation de données.